﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Formatters.Binary;
using System.Threading.Tasks;
using Newtonsoft.Json;
using StackExchange.Redis;

#if NETSTANDARD2_0 || NETSTANDARD2_1

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Json;

#else

using System.Configuration;

#endif

namespace HisPro.RedisHelp
{
    public class RedisCacheImp : ICacheProvider
    {
        #region var
        private IDatabase cache;
        private ConnectionMultiplexer connection;
        /// <summary>
        /// 数据库
        /// </summary>
        private int DbIndex { get; }
        private string DBName { get; }
        public string KeySuffix { get; set; }
        /// <summary>
        /// redis 连接字符串
        /// 27.0.0.1:6379,192.168.80.130:6379,allowadmin=true,password=123456
        /// </summary>
        public string RedisConnectionString { get; set; }
        private static readonly ConcurrentDictionary<string, Lazy<ObjectPool<ConnectionMultiplexer>>> _pool =
            new ConcurrentDictionary<string, Lazy<ObjectPool<ConnectionMultiplexer>>>();
        #endregion

        #region RedisCacheImp
        public RedisCacheImp() : this(null, 0)
        {
        }
        public RedisCacheImp(string dbname, int dbIndex) : this(dbname, dbIndex, null)
        {
        }
        public RedisCacheImp(string redisConnectionString) : this(null, 0, redisConnectionString)
        {
        }
        public RedisCacheImp(string dbname, int dbIndex, string redisConnectionString)
        {
            if (dbname.IsNullOrEmpty())
                dbname = this.GetType().Name;
            DBName = dbname;
            if (dbIndex < 0)
                dbIndex = 0;
            this.DbIndex = dbIndex;

#if NETSTANDARD2_0 || NETSTANDARD2_1

            if(redisConnectionString.IsNullOrEmpty())
            {
                string _configPath = System.IO.Path.Combine(AppContext.BaseDirectory, "appsettings.json");
                IConfiguration s_ConfigJson = LoadJsonFile(_configPath);
                redisConnectionString = s_ConfigJson["RedisExchangeHosts"];
            }
            RedisConnectionString = redisConnectionString;

#else

            RedisConnectionString = redisConnectionString ?? ConfigurationManager.ConnectionStrings["RedisExchangeHosts"].ConnectionString;

#endif
            //connection = GetManager(redisConnectionString);
            //cache = connection.GetDatabase();
            cache = GetRedisClient(RedisConnectionString);
        }

#if NETSTANDARD2_0 || NETSTANDARD2_1
        private static IConfiguration LoadJsonFile(string FilePath)
        {
            var s_ConfigJson = new ConfigurationBuilder()
                .SetBasePath(System.AppContext.BaseDirectory)
                .AddJsonFile(FilePath, optional: false, reloadOnChange: true)
                .Build();
            return s_ConfigJson;
        }
#endif
        #endregion

        #region Set/Get
        /// <summary>
        /// 保存单个key value
        /// </summary>
        /// <param name="key">Redis Key</param>
        /// <param name="value">保存的值</param>
        /// <param name="expiry">过期时间</param>
        /// <returns></returns>
        public bool Set<T>(string key, T value, TimeSpan? expiry = default(TimeSpan?))
        {
            key = GetKeySuffix(key);
            if (expiry == null || expiry.HasValue == false)
            {
                return cache.StringSet(key, Serialize(value));
            }
            else
            {
                return cache.StringSet(key, Serialize(value), expiry);
            }
        }
        public T Get<T>(string key)
        {
            key = GetKeySuffix(key);
            return Deserialize<T>(cache.StringGet(key));
        }
        public T Get<T>(string key, Func<T> cachePopulate, TimeSpan? expiresTime, Func<T, bool> checkPopulate = null)
        {
            var cacheItem = Get<T>(key);
            if (!cacheItem.IsNullOrEmpty())
            {
                return cacheItem;
            }
            else if (cachePopulate != null)//调用fucloadData获取数据
            {
                var val = cachePopulate();
                if (!val.IsNullOrEmpty())//缓存
                {
                    if (checkPopulate == null || checkPopulate?.Invoke(val) == true)
                        Set(key, val, expiresTime);
                    return val;
                }
            }
            return default(T);
        }
        // <summary>
        /// 从redis获取数据
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="key"></param>
        /// <param name="val"></param>
        /// <returns></returns>
        public bool TryGet<T>(string key, out T val)
        {
            val = this.Get<T>(key);
            return !val.IsNullOrEmpty();
        }

        public bool Exists(string key)
        {
            key = GetKeySuffix(key);
            return cache.KeyExists(key);
        }
        public bool Remove(string key)
        {
            key = GetKeySuffix(key);
            return cache.KeyDelete(key);
        }
        /// <summary>
        /// 删除多个key
        /// </summary>
        /// <param name="keys">rediskey</param>
        /// <returns>成功删除的个数</returns>
        public long Remove(List<string> keys)
        {
            List<string> newKeys = keys.Select(GetKeySuffix).ToList();
            return cache.KeyDelete(ConvertRedisKeys(newKeys));
        }
        /// <summary>
        /// 删除所有缓存
        /// </summary>
        public bool RemoveAll()
        {
            cache.Execute("flushdb");
            return true;
        }
        #endregion

        #region Hash
        /// <summary>
        /// 判断某个数据是否已经被缓存
        /// </summary>
        /// <param name="key"></param>
        /// <param name="dataKey"></param>
        /// <returns></returns>
        public bool HashExists(string key, string dataKey)
        {
            key = GetKeySuffix(key);
            return cache.HashExists(key, dataKey);
        }
        /// <summary>
        /// 移除hash中的某值
        /// </summary>
        /// <param name="key"></param>
        /// <param name="dataKey"></param>
        /// <returns></returns>
        public bool RemoveHash(string key, string dataKey)
        {
            key = GetKeySuffix(key);
            return cache.HashDelete(key, dataKey);
        }

        public bool HashSet<T>(string key, string fieldKey, T fieldValue)
        {
            key = GetKeySuffix(key);
            string json = ConvertJson(fieldValue);
            return cache.HashSet(key, fieldKey, json);
        }


        /// <summary>
        /// 从hash表获取数据
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="key"></param>
        /// <param name="dataKey"></param>
        /// <returns></returns>
        public T HashGet<T>(string key, string dataKey)
        {
            key = GetKeySuffix(key);
            var value = cache.HashGet(key, dataKey);
            return ConvertObj<T>(value);
        }
        /// <summary>
        /// 获取dataKeys所有Value
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="key"></param>
        /// <returns></returns>
        public List<T> HashGet<T>(string key, List<string> dataKeys)
        {
            key = GetKeySuffix(key);
            var values = cache.HashGet(key, ConvertRedisValues(dataKeys));
            return ConvetList<T>(values.Select(s => s).ToArray());
        }

        /// <summary>
        /// 获取hashkey所有Redis Value
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="key"></param>
        /// <returns></returns>
        public Dictionary<string, T> HashGetAll<T>(string key)
        {
            key = GetKeySuffix(key);
            Dictionary<string, T> list = new Dictionary<string, T>();

            var values = cache.HashGetAll(key);
            if (values != null && values.Length > 0)
            {
                foreach (var val in values)
                {
                    list[val.Name] = ConvertObj<T>(val.Value);
                }
            }
            return list;// ConvetList<T>(values.Select(s => s.Value).ToArray());
        }

        /// <summary>
        /// 从redis获取数据
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="key"></param>
        /// <param name="dataKey"></param>
        /// <param name="val"></param>
        /// <returns></returns>
        public bool TryHashGet<T>(string key, string dataKey, out T val)
        {
            val = this.HashGet<T>(key, dataKey);
            return !val.IsNullOrEmpty();
        }
        /// <summary>
        /// 从redis获取数据
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="key"></param>
        /// <param name="dataKey"></param>
        /// <param name="val"></param>
        /// <returns></returns>
        public bool TryHashGet<T>(string key, List<string> dataKey, out List<T> val)
        {
            val = default(List<T>);
            if (dataKey?.Count > 0)
            {
                val = HashGet<T>(key, dataKey);
            }
            return !val.IsNullOrEmpty();
        }
        /// <summary>
        /// 获取hashkey所有Redis key
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="key"></param>
        /// <param name="val"></param>
        /// <returns></returns>
        public bool TryHashGetAll<T>(string key, out Dictionary<string, T> val)
        {
            val = HashGetAll<T>(key);
            if (!val.IsNullOrEmpty()) return true;
            return !val.IsNullOrEmpty();
        }
        /// <summary>
        /// 获取hashkey所有cache key
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="key"></param>
        /// <param name="val"></param>
        /// <returns></returns>
        public bool TryHashGetAll<T>(string key, out List<T> val)
        {
            val = default(List<T>);
            if (this.TryHashGetAll<T>(key, out Dictionary<string, T> dicval) == true)
            {
                val = dicval.Values.ToList();
            }
            return !val.IsNullOrEmpty();
        }
        /// <summary>
        /// 批量删除
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="key"></param>
        /// <param name="listdataKey"></param>
        /// <returns></returns>
        public bool HashDeleteExecute(string key, List<string> listdataKey)
        {
            if (listdataKey == null || listdataKey.Count == 0) return false;
            key = GetKeySuffix(key);

            var tran = cache.CreateTransaction();
            foreach (var dataKey in listdataKey)
            {
                if (dataKey != null && dataKey.Length > 0)
                    tran.HashDeleteAsync(key, dataKey);
            }
            return tran.ExecuteAsync().Result;
        }
        /// <summary>
        /// 批量删除
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="listdataKey"></param>
        /// <returns></returns>
        public bool HashDeleteExecute(Dictionary<string, List<string>> listdataKey)
        {
            if (listdataKey == null || listdataKey.Count == 0) return false;
            foreach (var dic in listdataKey)
            {
                var ret = HashDeleteExecute(dic.Key, dic.Value);
            }
            return true;
        }
        /// <summary>
        /// 批量更新
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="DellistdataKey">批量删除</param>
        /// <param name="dicHashEntrys">批量保存</param>
        /// <returns></returns>
        public bool HashUpdateExecute<T>(Dictionary<string, List<string>> DellistdataKey, Dictionary<string, Dictionary<string, T>> dicHashEntrys)
        {
            if (DellistdataKey == null && DellistdataKey.Count == 0 && dicHashEntrys == null && dicHashEntrys.Count == 0) return false;

            if (DellistdataKey?.Count > 0)//删除
            {
                HashDeleteExecute(DellistdataKey);
            }
            if (dicHashEntrys?.Count > 0)//更新
            {
                return HashSetExecute(dicHashEntrys);
            }
            return true;
        }
        /// <summary>
        /// 批量保存
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="key"></param>
        /// <param name="dicHashEntrys"></param>
        /// <returns></returns>
        public bool HashSetExecute<T>(string key, Dictionary<string, T> dicHashEntrys)
        {
            if (dicHashEntrys == null || dicHashEntrys.Count == 0) return false;
            key = GetKeySuffix(key);

            var tran = cache.CreateTransaction();
            var lstHashEntry = new List<HashEntry>();
            foreach (var dic in dicHashEntrys)
            {
                lstHashEntry.Add(new HashEntry(dic.Key, ConvertJson(dic.Value)));
            }
            tran.HashSetAsync(key, lstHashEntry.ToArray());
            return tran.ExecuteAsync().Result;
        }
        /// <summary>
        /// 批量保存
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="dicHashEntrys"></param>
        /// <returns></returns>
        public bool HashSetExecute<T>(Dictionary<string, Dictionary<string, T>> dicHashEntrys)
        {
            if (dicHashEntrys == null || dicHashEntrys.Count == 0) return false;
            foreach (var dic in dicHashEntrys)
            {
                var ret = HashSetExecute(dic.Key, dic.Value);
            }
            return true;
        }
        #endregion

        #region LockTake
        public bool LockRelease(string key, string value)
        {
            key = GetKeySuffix(key);
            var lockName = key + "_lock"; //unique lock name. key-relative.
            RedisValue lockToken = (value.IsNullOrEmpty() ? value : Guid.NewGuid().ToString());
            return cache.LockRelease(lockName, lockToken);
        }
        public bool LockTake(string key, string value, TimeSpan? expiry)
        {
            key = GetKeySuffix(key);
            var lockName = key + "_lock"; //unique lock name. key-relative.
            RedisValue lockToken = (value.IsNullOrEmpty() ? value : Guid.NewGuid().ToString());
            return cache.LockTake(lockName, lockToken, expiry ?? TimeSpan.FromSeconds(10));
        }

        public IDisposable AcquireLock(string key, TimeSpan timeOut, bool useLock = true)
        {
            return new CacheLock(this, key, timeOut, useLock);
        }

        public T GetCachedAndModifyWithLock<T>(string key, Func<T> retrieveDataFunc, TimeSpan timeExpiration,
            Func<T, bool> modifyEntityFunc, TimeSpan? lockTimeout = null, bool isSlidingExpiration = false) where T : class
        {

            int lockCounter = 0;//for logging in case when too many locks per key
            Exception logException = null;

            var lockToken = Guid.NewGuid().ToString(); //unique token for current part of code
            var lockName = key + "_lock"; //unique lock name. key-relative.
            T tResult = null;

            while (lockCounter < 20)
            {
                //check for access to cache object, trying to lock it
                if (!cache.LockTake(lockName, lockToken, lockTimeout ?? TimeSpan.FromSeconds(10)))
                {
                    lockCounter++;
                    System.Threading.Thread.Sleep(100); //sleep for 100 milliseconds for next lock try. you can play with that
                    continue;
                }

                try
                {
                    RedisValue result = RedisValue.Null;

                    if (isSlidingExpiration)
                    {
                        //in case of sliding expiration - get object with expiry time
                        var exp = cache.StringGetWithExpiry(key);

                        //check ttl.
                        if (exp.Expiry.HasValue && exp.Expiry.Value.TotalSeconds >= 0)
                        {
                            //get only if not expired
                            result = exp.Value;
                        }
                    }
                    else //in absolute expiration case simply get
                    {
                        result = cache.StringGet(key);
                    }

                    //"REDIS_NULL" is for cases when our retrieveDataFunc function returning null (we cannot store null in redis, but can store pre-defined string :) )
                    if (result.HasValue && result == "REDIS_NULL") return null;
                    //in case when cache is epmty
                    if (!result.HasValue)
                    {
                        //retrieving data from caller function (from db from example)
                        tResult = retrieveDataFunc();

                        if (tResult != null)
                        {
                            //trying to modify that entity. if caller modifyEntityFunc returns true, it means that caller wants to resave modified entity.
                            if (modifyEntityFunc(tResult))
                            {
                                //json serialization
                                var json = JsonConvert.SerializeObject(tResult);
                                cache.StringSet(key, json, timeExpiration);
                            }
                        }
                        else
                        {
                            //save pre-defined string in case if source-value is null.
                            cache.StringSet(key, "REDIS_NULL", timeExpiration);
                        }
                    }
                    else
                    {
                        //retrieve from cache and serialize to required object
                        tResult = JsonConvert.DeserializeObject<T>(result);
                        //trying to modify
                        if (modifyEntityFunc(tResult))
                        {
                            //and save if required
                            var json = JsonConvert.SerializeObject(tResult);
                            cache.StringSet(key, json, timeExpiration);
                        }
                    }

                    //refresh exiration in case of sliding expiration flag
                    if (isSlidingExpiration)
                        cache.KeyExpire(key, timeExpiration);
                }
                catch (Exception ex)
                {
                    logException = ex;
                }
                finally
                {
                    cache.LockRelease(lockName, lockToken);
                }
                break;
            }

            if (lockCounter >= 20 || logException != null)
            {
                //log it
            }

            return tResult;
        }
        #endregion

        #region 辅助方法
        /// <summary>
        /// 设置前缀
        /// </summary>
        /// <param name="customKey"></param>
        public void SetSysCustomKey(string customKey)
        {
            KeySuffix = customKey;
        }
        private RedisKey[] ConvertRedisKeys(List<string> redisKeys)
        {
            return redisKeys.Select(redisKey => (RedisKey)redisKey).ToArray();
        }
        private RedisValue[] ConvertRedisValues(List<string> redisValues)
        {
            return redisValues.Select(redisKey => (RedisValue)redisKey).ToArray();
        }

        #region GetManager
        private IDatabase GetRedisClient(string redisConnectionString = null, int minSize = 100, int maxSize = 100)
        {
            try
            {
                redisConnectionString = redisConnectionString ?? RedisConnectionString;

                var key = $"{DBName}{DbIndex}";
                if (!_pool.ContainsKey(key) || connection == null)
                {
                    var objectPool = new Lazy<ObjectPool<ConnectionMultiplexer>>(() => new ObjectPool<ConnectionMultiplexer>(() =>
                    {
                        var connection = GetManager(redisConnectionString);
                        return connection;
                    }, minSize, maxSize));
                    _pool.GetOrAdd(key, objectPool);

                    connection = objectPool.Value.GetObject();
                    return connection.GetDatabase(DbIndex);
                }
                else
                {
                    connection = _pool[key].Value.GetObject();
                    return connection.GetDatabase(DbIndex);
                }
            }
            catch (Exception e)
            {
                throw e;
            }
        }
        private ConnectionMultiplexer GetManager(string redisConnectionString = null)
        {
            redisConnectionString = redisConnectionString ?? RedisConnectionString;
            var connect = ConnectionMultiplexer.Connect(redisConnectionString);

            //注册如下事件
            connect.ConnectionFailed += MuxerConnectionFailed;
            connect.ConnectionRestored += MuxerConnectionRestored;
            connect.ErrorMessage += MuxerErrorMessage;
            connect.ConfigurationChanged += MuxerConfigurationChanged;
            connect.HashSlotMoved += MuxerHashSlotMoved;
            connect.InternalError += MuxerInternalError;

            return connect;
        }
        #region 事件
        /// <summary>
        /// 配置更改时
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private static void MuxerConfigurationChanged(object sender, EndPointEventArgs e)
        {
            Console.WriteLine("Configuration changed: " + e.EndPoint);
        }

        /// <summary>
        /// 发生错误时
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private static void MuxerErrorMessage(object sender, RedisErrorEventArgs e)
        {
            Console.WriteLine("ErrorMessage: " + e.Message);
        }

        /// <summary>
        /// 重新建立连接之前的错误
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private static void MuxerConnectionRestored(object sender, ConnectionFailedEventArgs e)
        {
            Console.WriteLine("ConnectionRestored: " + e.EndPoint);
        }

        /// <summary>
        /// 连接失败 ， 如果重新连接成功你将不会收到这个通知
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private static void MuxerConnectionFailed(object sender, ConnectionFailedEventArgs e)
        {
            Console.WriteLine("重新连接：Endpoint failed: " + e.EndPoint + ", " + e.FailureType + (e.Exception == null ? "" : (", " + e.Exception.Message)));
        }

        /// <summary>
        /// 更改集群
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private static void MuxerHashSlotMoved(object sender, HashSlotMovedEventArgs e)
        {
            Console.WriteLine("HashSlotMoved:NewEndPoint" + e.NewEndPoint + ", OldEndPoint" + e.OldEndPoint);
        }

        /// <summary>
        /// redis类库错误
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private static void MuxerInternalError(object sender, InternalErrorEventArgs e)
        {
            Console.WriteLine("InternalError:Message" + e.Exception.Message);
        }

        #endregion 事件
        #endregion

        private string GetKeySuffix(string key)
        {
            return string.IsNullOrEmpty(KeySuffix) ? key : string.Format("_{0}_{1}", KeySuffix, key);
        }

        private string ConvertJson<T>(T value)
        {
            var jsonOption = new JsonSerializerSettings()
            {
                ReferenceLoopHandling = ReferenceLoopHandling.Ignore
            };
            string result = value is string ? value.ToString() : JsonConvert.SerializeObject(value, jsonOption);
            return result;
        }

        private T ConvertObj<T>(RedisValue value)
        {
            if (value.IsNullOrEmpty) return default(T);
            if (typeof(T).Equals(typeof(string)))
            {
                return JsonConvert.DeserializeObject<T>($"'{value}'");
            }
            return JsonConvert.DeserializeObject<T>(value);
        }
        private List<T> ConvetList<T>(RedisValue[] values)
        {
            List<T> result = new List<T>();
            foreach (var item in values)
            {
                if (!item.IsNullOrEmpty)
                {
                    var model = ConvertObj<T>(item);
                    result.Add(model);
                }
            }
            return result;
        }
        private RedisValue Serialize(object o)
        {
            RedisValue redisValue = RedisValue.Null;
            if (o == null)
            {
                return redisValue;
            }
            var type = o.GetType();
            if (type.IsClass && !type.Equals(typeof(string)))
            {
                redisValue = ConvertJson(o);
            }
            else
            {
                redisValue = serialize(o);
            }
            return redisValue;
        }
        private T Deserialize<T>(RedisValue redisValue)
        {
            try
            {
                //var bx =redisValue.Box();
                //if (redisValue == null || redisValue == RedisValue.Null)
                //    return default(T);
                var type = typeof(T); 
                if (type.IsClass && !type.Equals(typeof(string)))
                {
                    return ConvertObj<T>(redisValue);
                }
                else
                {
                    return deserialize<T>(redisValue);
                }
            }
            catch { return default(T); }
        }
        private byte[] serialize(object o)
        {
            if (o == null)
            {
                return null;
            }

            BinaryFormatter binaryFormatter = new BinaryFormatter();
            using (MemoryStream memoryStream = new MemoryStream())
            {
                binaryFormatter.Serialize(memoryStream, o);
                byte[] objectDataAsStream = memoryStream.ToArray();
                return objectDataAsStream;
            }
        }

        private T deserialize<T>(byte[] stream)
        {
            if (stream == null)
            {
                return default(T);
            }

            BinaryFormatter binaryFormatter = new BinaryFormatter();
            using (MemoryStream memoryStream = new MemoryStream(stream))
            {
                T result = (T)binaryFormatter.Deserialize(memoryStream);
                return result;
            }
        }
        public void Dispose()
        {
            //if (connection != null)
            //{
            //    connection.Close();
            //}
            GC.SuppressFinalize(this);
        }
        #endregion

    }
}
